/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.editor.model;

import java.io.IOException;

import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.Style;
import javax.swing.text.html.HTML;

/**
 * @author Walter Lütgenau
 *
 */
public class ParagraphModel extends EditorModel {

    public ParagraphModel(DocumentContext context) {
        super(context);
    }

    public boolean isParagraph(Element e) {
        return e.getName().equals(HTML.Tag.P.toString()) || e.getName().equals(HTML.Tag.IMPLIED.toString()) || e.getName().equals(HTML.Tag.PRE.toString());
    }

    public boolean isStartOfParagraph(int pos) {
        Element paragraph = getDocument().getParagraphElement(pos);
        return paragraph.getStartOffset() == pos && paragraph.getParentElement().getName().equals(HTML.Tag.BODY.toString());
    }

    public boolean isEndOfHeading(int pos) {
        Element e = getDocument().getParagraphElement(pos);
        return e.getEndOffset() - 1 == pos && isHeading(e);
    }

    public boolean isHeading(Element e) {
        return e.getName().equals(HTML.Tag.H1.toString()) ||
                e.getName().equals(HTML.Tag.H2.toString()) ||
                e.getName().equals(HTML.Tag.H3.toString()) ||
                e.getName().equals(HTML.Tag.H4.toString()) ||
                e.getName().equals(HTML.Tag.H5.toString()) ||
                e.getName().equals(HTML.Tag.H6.toString());
    }

    public void setHeading(Range range, int level) throws BadLocationException, IOException {
        Element element = getDocument().getParagraphElement(range.getStartOffset());
        Element container = element.getParentElement();
        if (container.getName().equals(HTML.Tag.PRE.toString())) {
            container = container.getParentElement();
        }
        if (!isContainer(container)) {
            return;
        }

        for (int i = 0; i < container.getElementCount(); i++) {
            Element e = container.getElement(i);

            if (isWithinRange(e, range) && (isHeading(e) || isParagraph(e))) {
                StringBuffer buf = new StringBuffer("<h" + level + ">" + getInnerHTML(e) + "</h" + level + ">");
                getDocument().setOuterHTML(e, buf.toString());
            }
        }
    }

    public void setParagraph(Range range, String type) throws BadLocationException, IOException {
        Element first = getDocument().getParagraphElement(range.getStartOffset());
        Element parent = first.getParentElement();
        if (parent.getName().equals(HTML.Tag.PRE.toString())) {
            parent = parent.getParentElement();
        }

        for (int i = 0; i < parent.getElementCount(); i++) {
            Element e = parent.getElement(i);

            if ((isParagraph(e) || isHeading(e)) && isWithinRange(e, range)) {
                getDocument().setOuterHTML(e, "<" + type + ">" + stripEnvelopeWhiteSpace(getInnerHTML(e)) + "</" + type + ">");
            }
        }
    }

    public void addParagraphAfterHeading(int pos) throws BadLocationException, IOException {
        Element e = getDocument().getParagraphElement(pos);
        if (!isHeading(e)) {
            return;
        }
        addParagraphAfterElement(e);
    }

    public void addParagraphAfterElement(Element e) throws BadLocationException, IOException {
        getDocument().setOuterHTML(e, getOuterHTML(e) + "<p></p>");
    }

    public void addParagraphBeforeElement(Element e) throws BadLocationException, IOException {
        getDocument().setOuterHTML(e, "<p></p>" + getOuterHTML(e));
    }

    public Element findHeading(Element e) {
        // consider headings as well in divs and tables
        // 1. try to find previous sibling
        if ( e == null || e.getParentElement() == null)
            return null;
        
        int childIndex = getChildIndex(e);
        for( int i=childIndex; i>=0; i--) {
            Element child = e.getParentElement().getElement(i);
            if ( isHeading(child))
                return child;
        }
        
        // 2. try to find the heading on the parent level
        Element result = findHeading( e.getParentElement());
        return result;
    }

    /**
     * Calculate the heading level. E.g. a heading h2 has a level of 2.
     * 
     * @param heading The element, that represents the heading
     * @return The heading level
     */
    protected int getHeadingLevel(Element heading) {
        String h = heading.getName();
        if (h.charAt(0) != 'h') {
            return -1;
        }

        int level = -1;
        try {
            level = Integer.parseInt(h.substring(1));
        } catch (Exception e) {
            e.printStackTrace();
        }

        return level;
    }

    /**
     * Indent a heading. 
     * 
     * @param heading The (potentially) heading element to be indented. 
     * @param levels positive levels indicate indentation to higher heading numbers, negative heading levels vice versa
     * @throws BadLocationException
     * @throws IOException
     */
    protected void headingIndent(Element heading, int levels) throws BadLocationException, IOException {
        int level = getHeadingLevel(heading);

        if (level < 0) {
            return;
        } // no heading. Go on with the search

        if (level >= 10 || level <= 0) {
            return;
        } // no further indentation possible

        level += levels;

        StringBuffer buf = new StringBuffer("<h" + level + ">" + getInnerHTML(heading) + "</h" + level + ">");
        getDocument().setOuterHTML(heading, buf.toString());
    }

    /**
     * Indent all headings from the start heading up to the next heading of the same level.
     * 
     * @param heading The start heading
     * @param levels positive levels indicate indentation to higher heading numbers, negative heading levels vice versa
     * @throws BadLocationException
     * @throws IOException
     */
    protected void headingsIndent(Element heading, int levels) throws BadLocationException, IOException {
        int startLevel = getHeadingLevel(heading);
        if (startLevel < 0 || startLevel == 9) {
            return;
        }

        int index = getChildIndex(heading);

        Element parent = heading.getParentElement();
        for (int i = index; i < parent.getElementCount() - 1; i++) {
            int level = getHeadingLevel(parent.getElement(i));

            if (i != index && level == startLevel) {
                return;
            }

            headingIndent(parent.getElement(i), levels);
        }
    }

    /**
     * Indent all headings from the start heading up to the next heading of the same level one level up.
     * 
     * @param heading The start heading
     * @throws BadLocationException
     * @throws IOException
     */
    public void headingRightIndent(Element heading) throws BadLocationException, IOException {
        headingsIndent(heading, 1);
    }

    /**
     * Indent all headings from the start heading up to the next heading of the same level one level down.
     * 
     * @param heading The start heading
     * @throws BadLocationException
     * @throws IOException
     */
    public void headingLeftIndent(Element heading) throws BadLocationException, IOException {
        headingsIndent(heading, -1);
    }

    public void headingUp(Element heading) throws BadLocationException, IOException {
        int index = getChildIndex(heading);
        int startLevel = getHeadingLevel(heading);
        int start = heading.getStartOffset();
        int end = heading.getEndOffset();

        Element parent = heading.getParentElement();

        // Build buffer for actual heading and subheadings, calculate start and end offset
        StringBuffer buf = new StringBuffer();
        for (int i = index; i < parent.getElementCount(); i++) {
            int level = getHeadingLevel(parent.getElement(i));

            if (i != index && level == startLevel) {
                break;
            }

            buf.append(getOuterHTML(parent.getElement(i)));
            end = parent.getElement(i).getEndOffset();
        }

        if (end > getDocument().getLength()) {
            end = getDocument().getLength();
        }

        // find heading of same level upwards
        Element found = null;
        for (int i = index; i >= 0; i--) {
            int level = getHeadingLevel(parent.getElement(i));
            if (i != index && level == startLevel) {
                found = parent.getElement(i);
                break;
            }
        }

        if (found == null) {
            return;
        }

        buf.append(getOuterHTML(found));

        getDocument().remove(start, end - start);
        getDocument().setOuterHTML(found, buf.toString());
    }

    public void headingDown(Element heading) throws BadLocationException, IOException {
        int index = getChildIndex(heading);
        int startLevel = getHeadingLevel(heading);
        int start = heading.getStartOffset();
        int end = heading.getEndOffset();

        Element parent = heading.getParentElement();

        // Build buffer for actual heading and subheadings, calculate start and end offset
        StringBuffer buf = new StringBuffer();
        Element element = null;
        int i;
        for (i = index; i < parent.getElementCount(); i++) {
            element = parent.getElement(i);
            int level = getHeadingLevel(element);

            if (i != index && level == startLevel) {
                break;
            }

            buf.append(getOuterHTML(element));
            end = parent.getElement(i).getEndOffset();
        }

        if (i == parent.getElementCount()) {
            return;
        }

        // find heading of same level downwards again, starting from the next heading of the same level found before
        int endIndex = getChildIndex(element);
        for (i = endIndex; i < parent.getElementCount(); i++) {
            int level = getHeadingLevel(parent.getElement(i));
            if (i != endIndex && level == startLevel) {
                break;
            }
        }

        if (i < parent.getElementCount()) { // found the next heading
            heading = parent.getElement(i);
            buf.append(getOuterHTML(heading));
            getDocument().setOuterHTML(heading, buf.toString());
        } else { // at end
            element = parent.getElement(i - 1);
            buf.insert(0, getOuterHTML(element));
            getDocument().setOuterHTML(element, buf.toString());
        }

        getDocument().remove(start, end - start);
    }
    
    public void setStyle(Range range, String styleClassName, boolean isClassStyle) throws BadLocationException, IOException {
        Element e = getParagraphWithNoImpliedSiblings(getDocument().getParagraphElement(range.getStartOffset()));

        setStyle(e, styleClassName, isClassStyle);
      }

    public void setStyle(Element e, String styleClassName, boolean isClassStyle) throws BadLocationException, IOException {
        SimpleAttributeSet atts = new SimpleAttributeSet(e.getAttributes());

        HTML.Attribute styleAttribute;
        if (isClassStyle)
            styleAttribute = HTML.Attribute.CLASS;
        else
            styleAttribute = HTML.Attribute.ID;

        atts.removeAttribute(styleAttribute);
        atts.addAttribute(styleAttribute, styleClassName);

        StringBuffer buf = new StringBuffer();
        buf.append(getStartTag(atts));
        buf.append(getInnerHTML(e));
        buf.append(getEndTag(e));

        setOuterHTML(e, buf.toString());
    }

    public void removeStyle(Element e) throws BadLocationException, IOException {
      SimpleAttributeSet atts = new SimpleAttributeSet(e.getAttributes());

        atts.removeAttribute(HTML.Attribute.CLASS);
        atts.removeAttribute(HTML.Attribute.ID);

        StringBuilder buf = new StringBuilder();
        buf.append(getStartTag(atts));
        buf.append(getInnerHTML(e));
        buf.append(getEndTag(e));

        setOuterHTML(e, buf.toString());
    }

    public void createContainerDiv(Element parent, int min, int max) throws BadLocationException, IOException {
        StringBuffer buf = new StringBuffer();
        
        buf.append(getStartTag(parent));
        for( int i=0; i<parent.getElementCount(); i++) {
            if ( i == min)
                buf.append("<div>");
            buf.append(getOuterHTML(parent.getElement(i)));
            if ( i == max)
                buf.append("</div");
        }
        buf.append(getEndTag(parent));
        
        setOuterHTML(parent, buf.toString());
    }
}

